/*
 * Copyright (c) 2005, 2014 vacoor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */
package org.vacoor.nothing.common.crypto.digest;

import org.vacoor.nothing.common.codec.Base64;
import org.vacoor.nothing.common.codec.Hex;
import org.vacoor.nothing.common.util.Bytes;
import org.vacoor.nothing.common.util.Throwables;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * 非线程安全 消息摘要 封装
 * <p/>
 * new Hash.MD5(plain, salt).toHex()
 *
 * @author vacoor
 */
@SuppressWarnings({"unused"})
public class Hash {

    public enum Algorithm {
        // Message Digest Algorithm, Secure Hash Algorithm
        MD2("MD2"), MD5("MD5"), SHA1("SHA-1"), SHA256("SHA-256"), SHA384("SHA-384"), SHA512("SHA-512");

        private final String name;

        Algorithm(String algorithm) {
            this.name = algorithm;
        }
    }

    /**
     * MD5
     */
    public static class MD5 extends Hash {
        public MD5(Object source) {
            this(source, null);
        }

        public MD5(Object source, Object salt) {
            this(source, salt, DEFAULT_ITERATIONS);
        }

        public MD5(Object source, Object salt, int hashIterations) {
            super(Algorithm.MD5, source, salt, hashIterations);
        }
    }

    /**
     * SHA-1
     */
    public static class SHA1 extends Hash {
        public SHA1(Object source) {
            this(source, null);
        }

        public SHA1(Object source, Object salt) {
            this(source, salt, DEFAULT_ITERATIONS);
        }

        public SHA1(Object source, Object salt, int hashIterations) {
            super(Algorithm.SHA1, source, salt, hashIterations);
        }
    }

    /**
     * SHA-256
     */
    public static class SHA256 extends Hash {

        public SHA256(Object source) {
            this(source, null);
        }

        public SHA256(Object source, Object salt) {
            this(source, salt, DEFAULT_ITERATIONS);
        }

        public SHA256(Object source, Object salt, int hashIterations) {
            super(Algorithm.SHA256, source, salt, hashIterations);
        }
    }

    // -------------------
    protected static final int DEFAULT_ITERATIONS = 1;

    private byte[] bytes;   // hashed bytes
    private String hexEncoded;
    private String base64Encoded;

    public Hash(Algorithm algorithm, Object source) {
        this(algorithm, source, null);
    }

    public Hash(Algorithm algorithm, Object source, Object salt) {
        this(algorithm, source, salt, DEFAULT_ITERATIONS);
    }

    public Hash(Algorithm algorithm, Object source, Object salt, int hashIterations) {
        if (algorithm == null || source == null) {
            throw new NullPointerException("algorithm and source argument cannot be null.");
        }

        byte[] sourceBytes = Bytes.toBytes(source);
        byte[] saltBytes = salt == null ? null : Bytes.toBytes(salt);
        hashIterations = Math.max(hashIterations, DEFAULT_ITERATIONS);

        hash(algorithm, sourceBytes, saltBytes, hashIterations);
    }

    protected void hash(Algorithm algorithm, byte[] source, byte[] salt, int iterations) {
        try {
            bytes = hash(algorithm.name, source, salt, iterations);
        } catch (NoSuchAlgorithmException e) {
            throw Throwables.unchecked(e);
        }
    }

    protected byte[] hash(String algorithm, byte[] bytes, byte[] salt, int hashIterations) throws NoSuchAlgorithmException {
        MessageDigest digester = MessageDigest.getInstance(algorithm);

        if (salt != null) {
            digester.reset();
            digester.update(salt);
        }
        byte[] hashed = digester.digest(bytes);

        for (int i = 0; i < hashIterations - 1; i++) {
            digester.reset();
            hashed = digester.digest(hashed);
        }

        return hashed;
    }

    public byte[] getBytes() {
        return bytes;
    }

    public String toHex() {
        if (null == hexEncoded) {
            hexEncoded = Hex.encode(bytes);
        }

        return hexEncoded;
    }

    public String toBase64() {
        if (null == base64Encoded) {
            base64Encoded = Base64.encodeToString(bytes);
        }

        return base64Encoded;
    }
}
